feat(notify): Teams DM bell beside failure emails (T1) #27
+390
−0
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Why
archive.pyhad zero Teams/webhook code — every[FAILED]/[STALE]/[EXCLUSION-ABORT]went out only as email (send_alert_email()→ smtplib → smtp.purdue.edu), which gets tuned out. This is the "backup didn't happen" T1 tier of the settled notification-triage strategy and the highest-value gap: what actually stops must-see failure signals being missed.What
A small stdlib-only
notify_bell()that POSTs a Teams Adaptive Card beside the durable audit email. The email stays as the record; the bell is the attention signal. Card title is prefixedACT NOW:.Delivered as a 1:1 Teams DM, not a channel @mention (routing decision 2026-07-11; mirrors merged florasense-tools PR #106
--dmmode). The card carries no<at>/ nomsteams.entities: a DM already notifies, and a Power-Automate flow-bot chat post renders<at>entities as literal text. DM-vs-channel is decided by the Power Automate flow behindEARLY_TEAMS_WEBHOOK, not the payload — flipping to a DM is a Power-Automate web-UI step done separately from this code.notify_bell()/build_teams_card()—urllib.requestonly, imports inside the body (engine invariant chore(reclaim): hold AAPF landing-zone raw until promote loop verified #1). Card shape reused fromflorasense-tools/fortress/reship_watch_alert.py.EARLY_TEAMS_WEBHOOK— silent no-op (no POST) until wired into the run env on Negishi. No mention env vars needed or read.{200,202}= delivered; any error (absent/failed webhook, network) degrades to email-only with a logged reason and never raises. The webhook is a secret (sig) and is never printed/logged.send_alert_email()failure site. The size-routing completion email ([SUCCESS]/[RESHIP-WATCH]) is deliberately not belled.send_to_fortress(self-contained, closes overwrite_logonly, imports inside) for the exclusion companion-guard abort — the one alert that continues the run (so it never reaches the local Step-2 wrapper) and can fire on a compute node under--globus. All other verification[FAILED]sraise→ the Step-2 wrapper alerts and bells.Tests
tests/test_notify_bell.py— 14 tests (POST mocked,os.environfully controlled):<at>/nomsteams.entities, legacy mention env vars ignored),ACT NOW:prefix + idempotency, HTTP 200/202 success, never-raises onHTTPError/URLError/ generic exception + non-2xx, and webhook-never-printed secret safety.python3 -m unittest discover -s tests→ 289 passed.Deploy note (default-off)
Does nothing until
EARLY_TEAMS_WEBHOOKis exported into the run environment on Negishi (archive_local.sbatch/ cron), and the Power Automate flow behind it is set to post a DM. Until thennotify_bell()is a silent no-op and email behavior is unchanged.Scope guards
--dm/--taskmodes — triage design keeps everything on the one alert surface.🤖 Generated with Claude Code